#!/usr/bin/env python
#
#       MajeRe_Gui.py
#       
#       Copyright 2009 Brandon Invergo <brandon@brandoninvergo.com>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

import sys

try:
    import pygtk
    #tell pyGTK, if possible, that we want GTKv2
    pygtk.require("2.0")
except:
    #Some distributions come with GTK2, but not pyGTK
    pass

try:
    import gtk
    import gtk.glade
    
except:
    print "You need to install pyGTK or GTKv2 ",
    print "or set your PYTHONPATH correctly."
    print "try: export PYTHONPATH=",
    print "/usr/local/lib/python2.2/site-packages/"
    sys.exit(1)

import gtksourceview
from gtk.gdk import CONTROL_MASK, SHIFT_MASK
from gobject import TYPE_INT, TYPE_STRING
import MajeRe_Engine
import locale

class MajeRe_Gui:
    """This class constructs the MajeRe GUI"""
    script_buffer = gtksourceview.SourceBuffer()
    console_buffer = gtk.TextBuffer()
    r_eng = MajeRe_Engine.MajeRe_Engine()
    r_input_waiting = False
    open_brackets = ["{", "[", "("]
    closed_brackets = ["}", "]", ")"]
    bracket_counts = [0, 0, 0]
    command_history = [""]
    command_history_mark = -1
    clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
    data_objects = set()
    encoding = locale.getlocale()[1]
    utf8conv = lambda self, x : unicode(x, self.encoding).encode('utf8')

    
    def on_console_entry(self, widget, entry):
        """This function handles commands entered in the console input"""
        entry_text = entry.get_text()
        self.do_R_command(entry_text)
        entry.set_text("")
    
    def on_script_key_event(self, widget, event):
        """This function handles key events sent from the script_buffer"""
        key = gtk.gdk.keyval_name(event.keyval)
        if event.state & CONTROL_MASK:
            # If Ctrl-Return is pressed, send a command to R and output
            # to the console
            if key == "Return":
                # If text is selected, split the selection by lines,
                # send each line individually to R, and output to the 
                # console
                if self.script_buffer.get_has_selection():
                    selection_bounds = self.script_buffer.get_selection_bounds()
                    selected_text = self.script_buffer.get_text(selection_bounds[0], \
                        selection_bounds[1])
                    selected_lines = selected_text.splitlines(False)
                    for line in selected_lines:
                        self.do_R_command(line)
                # ...otherwise, send the text in the current line of the
                # script editor
                else:
                    cursor_iter = self.script_buffer.get_iter_at_mark(self.script_buffer.get_insert())
                    cursor_line = cursor_iter.get_line()
                    line_iter = self.script_buffer.get_iter_at_line(cursor_line)
                    end_line_iter = line_iter.copy()
                    end_line_iter.forward_to_line_end()
                    script_entry = self.script_buffer.get_text(line_iter, \
                            end_line_iter)
                    self.do_R_command(script_entry)
                    if end_line_iter.forward_line() == False:
                        self.script_buffer.insert(end_line_iter,"\n")
                        end_line_iter.forward_line()
                    self.script_buffer.place_cursor(end_line_iter)
                return True
        elif event.state & SHIFT_MASK:
            if key == "braceleft":
                cursor_iter = self.script_buffer.get_iter_at_mark(self.script_buffer.get_insert())
                self.script_buffer.insert(cursor_iter,"{\n\t")
                return True
            if key == "braceright":
                cursor_iter = self.script_buffer.get_iter_at_mark(self.script_buffer.get_insert())
                new_char = cursor_iter.copy()
                new_char.backward_char()
                if new_char.get_char() == "\t":
                    self.script_buffer.delete(new_char,cursor_iter)
                    self.script_buffer.insert(cursor_iter, "}\n")
                else:
                    self.script_buffer.insert(cursor_iter, "\n}\n")
                return True
        return False
    
    def on_menu_open_script(self, widget):
        self.open_script_dialog.show()
        
    def on_open_script_open_button(self, widget):
        try:
            opened_script = open(self.open_script_dialog.get_filename(),"r")
        except:
            self.show_error("Error opening file")
        content = opened_script.read()
        try:
            content = self.utf8conv(content)  
        except:
            self.show_error("Unsupported file type.")
        else:
            self.script_buffer.set_text(content)
            opened_script.close()
            self.open_script_dialog.hide()
    
    def on_open_script_cancel_button(self, widget):
        self.open_script_dialog.hide()
        
    def on_menu_export_script(self, widget):
        self.export_script_dialog.show()
    
    def on_export_script_save_button(self, widget):
        try:
            new_file = open(self.export_script_dialog.get_filename(), "w")
        except:
            self.show_error("Error opening file for writing.")
        else:
            start_iter = self.script_buffer.get_start_iter()
            end_iter = self.script_buffer.get_end_iter()
            try:
                new_file.write(self.script_buffer.get_text(start_iter,end_iter))
            except:
                self.show_error("Error writing file.")
            else:
                self.export_script_dialog.hide()
        
        
    def on_export_script_cancel_button(self, widget):
        self.export_script_dialog.hide()
        
    def on_data_objects_selected(self, widget):
        self.data_objects_edit_button.set_sensitive(True)
        self.data_objects_remove_button.set_sensitive(True)
        
    def on_data_objects_remove(self, widget):
        cursor = self.dt_treeview.get_cursor()[0]
        tree_iter = self.dt_treestore.get_iter(cursor)
        object_name = self.dt_treestore.get_value(tree_iter,0)
        if not self.r_eng.delete_object(object_name):
            self.show_error("Error deleting object")
        else:
            self.update_data_view()
        
    
    def on_menu_cut(self, widget):
        self.script_buffer.cut_clipboard(self.clipboard, True)
        
    def on_menu_copy(self, widget):
        self.script_buffer.copy_clipboard(self.clipboard)
        
    def on_menu_paste(self, widget):
        self.script_buffer.paste_clipboard(self.clipboard, None, True)
        
    def on_menu_undo(self, widget):
        if self.script_buffer.can_undo():
            self.script_buffer.undo()
            
    def on_menu_redo(self, widget):
        if self.script_buffer.can_redo():
            self.script_buffer.redo()
    
    def on_console_key_event(self, widget, event):
        key = gtk.gdk.keyval_name(event.keyval)
        if key == "Up":
            self.command_history_mark = self.command_history_mark + 1
            if self.command_history_mark == len(self.command_history):
                self.command_history_mark = 0
            widget.set_text(self.command_history[self.command_history_mark])
            return True
        elif key == "Down":
            self.command_history_mark = self.command_history_mark - 1
            if self.command_history_mark < 0:
                self.command_history_mark = len(self.command_history) - 1
            widget.set_text(self.command_history[self.command_history_mark])
            return True
        return False
            
    def on_menu_undo(self, widget):
        self.script_buffer.undo()
    
    def close_application(self, widget):
        gtk.main_quit()
             
    def show_error(self, error_string):
        self.error_label.set_text(error_string)
        self.error_dialog.show()
        
    def on_error_button(self, widget):
        self.error_dialog.hide()
        
    def do_R_command(self, command):
        """This function interacts with R by sending commands to it"""
        command = command.lstrip('\t')
        
        if command == "":
            return False
        
        if self.r_input_waiting == False:
            command_output = '>' + command + '\n'
        else:
            command_output = '... ' + command + '\n'
            self.chain_command = self.chain_command + command
        
        for i in range(0,len(self.open_brackets)):
            self.bracket_counts[i] = self.bracket_counts[i] + \
                command.count(self.open_brackets[i])
        for i in range(0,len(self.closed_brackets)):
            self.bracket_counts[i] = self.bracket_counts[i] - \
                command.count(self.closed_brackets[i])
        
        if len(filter(lambda x: x > 0, self.bracket_counts)) != 0:
            if self.r_input_waiting == False:
                self.r_input_waiting = True
                self.chain_command = command
        else:
            if self.r_input_waiting == True:
                self.r_input_waiting = False
                command = self.chain_command
                self.chain_command = ""

        enditer = self.console_buffer.get_end_iter()
        self.console_buffer.insert(enditer, command_output)
        bold_begin = enditer.copy()
        bold_begin.backward_line()
        bold_end = bold_begin.copy()
        bold_end.forward_to_line_end()
        # Make the command bold (keep the results normal)
        self.console_buffer.apply_tag_by_name("bold", bold_begin, bold_end)
        if self.r_input_waiting == False:
            self.command_history.insert(0, command)
            self.command_history_mark = 0
            res = self.r_eng.R(command)
            if (res[0] == False):
                self.console_buffer.insert(enditer, "Error: " + res[1] + "\n")
            else:
                for line in res[1]:
                    self.console_buffer.insert(enditer, line + "\n")
            self.update_data_view()
            return res
        return False
    
    def match_datatree_val(self, model, path, iter, data):
        if model.get_value(iter, 0) == data[0]:
            data[1].append(path)
        return False         
    
    
    def append_data_treeview(self, new_objects, parent):
        for obj in new_objects:
            if type(obj) == type(str()):
                object_type = self.r_eng.get_type(obj)
                object_len = self.r_eng.get_length(obj)
                dt_iter = self.dt_treestore.append(parent, \
                    [obj, object_type, object_len])
                parent_path = self.dt_treestore.get_path(parent)
                parent_iter = self.dt_treestore.get_iter(parent_path)
                if parent_path == self.data_sets_ref.get_path():
                    count = self.dt_treestore.get(parent_iter, 2)[0] + 1
                    self.dt_treestore.set_value(parent_iter, 2, count)
                elif parent_path == self.other_objects_ref.get_path():
                    count = self.dt_treestore.get(parent_iter, 2)[0] + 1
                    self.dt_treestore.set_value(parent_iter, 2, count)    
            elif type(obj) == type(list()):
                self.append_data_treeview(obj, dt_iter)
        return
    
    def dt_namecol_func(self, column, cell, model, iter):
        if model.iter_parent(iter) == None:
            name = model.get_value(iter, 0)
            cell.set_property('markup', "<b>" + name + "</b>")
        return

    def update_data_view(self):
        """This function updates the data tab with new objects that have
        been added and removes objects that no longer exist. This needs to be improved to handle data sets which 
        have members"""
        data_objects_temp = self.r_eng.get_objects()
        new_objects = list(data_objects_temp - self.data_objects)
        new_objects_copy = list(new_objects)
        new_data_frames = list()
        for obj in new_objects_copy:
            is_data_frame = self.r_eng.is_data_frame(obj)
            if is_data_frame == True:
                new_data_frames.append(obj)
                names = self.r_eng.get_dataframe_names(obj)
                new_data_frames.append(names)
                new_objects.remove(obj)
        if len(new_objects) > 0:
            other_objects_path = self.other_objects_ref.get_path()
            other_objects_iter = self.dt_treestore.get_iter(other_objects_path)
            self.append_data_treeview(new_objects, other_objects_iter)
            self.data_objects = self.data_objects | set(new_objects)
        if len(new_data_frames) > 0:
            data_sets_path = self.data_sets_ref.get_path()
            data_sets_iter = self.dt_treestore.get_iter(data_sets_path)
            self.append_data_treeview(new_data_frames, data_sets_iter)
            data_frame_names = list()
            for i in new_data_frames:
                if type(i) == type(str()):
                    data_frame_names.append(i)
            self.data_abjects = self.data_objects | set(data_frame_names)
        
        old_objects = list(self.data_objects - data_objects_temp)
        if len(old_objects) > 0:
            for obj in old_objects:
                pathlist = []
                self.dt_treestore.foreach(self.match_datatree_val, \
                    (obj, pathlist))
                pathlist.reverse()
                for path in pathlist:
                    self.dt_treestore.remove(self.dt_treestore.get_iter(path))
            self.data_objects = self.data_objects - set(old_objects)
        self.dt_treeview.show()
        
        
    def init_script_buffer(self):
        """This function initializes some settings for the script text buffer"""
        lang_man = gtksourceview.SourceLanguagesManager()
        langs = lang_man.get_available_languages()
        lang_names = [x.get_name() for x in langs]
        R_lang_index = lang_names.index("R")
        if R_lang_index:
            self.script_buffer.set_language(langs[R_lang_index])
        self.script_buffer.set_check_brackets(True)
        self.script_buffer.set_highlight(True)        
        
    def __init__(self):
        """This function initializes and displays the GUI"""
        self.init_script_buffer()
        self.console_buffer.create_tag("bold", weight=700) 
        gladefile="MajeRe.glade"
        
        # Load the Glade GUI
        self.glade_gui = gtk.glade.XML(gladefile)
        
        # Load the main MajeRe window
        self.main_window = self.glade_gui.get_widget("Main_window")
        
        # A GtkSourceView widget is added the hard way
        script_textview = gtksourceview.SourceView(self.script_buffer)
        script_textview.set_show_line_numbers(True)
        script_textview.set_auto_indent(True)
        script_textview.set_tabs_width(4)
        script_textview.set_smart_home_end(True)
        script_textview.connect("key_press_event", self.on_script_key_event)
        script_scroll = self.glade_gui.get_widget("Script_scrolledwindow")
        script_scroll.add_with_viewport(script_textview)
        script_textview.show()
        
        # Set up the R console
        console_output = self.glade_gui.get_widget("R_console_output")
        console_output.set_buffer(self.console_buffer)
        
        # Set up the data tables tree
        self.dt_treestore = gtk.TreeStore(TYPE_STRING, TYPE_STRING, \
            TYPE_INT)
            
        dt_iter = self.dt_treestore.append(None, \
            ["Data sets","",0])
        data_sets_path = self.dt_treestore.get_path(dt_iter)
        self.data_sets_ref = gtk.TreeRowReference(self.dt_treestore, \
            data_sets_path)            
        
        dt_iter = self.dt_treestore.append(None, \
            ["Other objects","",0])
        other_objects_path = self.dt_treestore.get_path(dt_iter)
        self.other_objects_ref = gtk.TreeRowReference(self.dt_treestore, \
            other_objects_path)            
            
        self.dt_treeview = self.glade_gui.get_widget("Data_objects_tree")
        self.dt_treeview.set_model(self.dt_treestore)
        dt_namecol = gtk.TreeViewColumn("Name")
        self.dt_treeview.append_column(dt_namecol)
        dt_namecell = gtk.CellRendererText()
        dt_namecol.pack_start(dt_namecell, True)
        dt_namecol.set_cell_data_func(dt_namecell, self.dt_namecol_func)

        
        dt_typecol = gtk.TreeViewColumn("Type")
        self.dt_treeview.append_column(dt_typecol)
        dt_typecell = gtk.CellRendererText()
        dt_typecol.pack_start(dt_typecell, True)
                
        dt_ncol = gtk.TreeViewColumn("n")
        self.dt_treeview.append_column(dt_ncol)
        dt_ncell = gtk.CellRendererText()
        dt_ncol.pack_start(dt_ncell, True)
        dt_namecol.add_attribute(dt_namecell, 'text', 0)
        dt_typecol.add_attribute(dt_typecell, 'text', 1)
        dt_ncol.add_attribute(dt_ncell, 'text', 2)
              
        main_callbacks = {"gtk_main_quit" : self.close_application, \
            "on_menu_open_script" : self.on_menu_open_script, \
            "on_menu_export_script" : self.on_menu_export_script, \
            "on_menu_copy" : self.on_menu_copy, \
            "on_menu_cut"  : self.on_menu_cut, \
            "on_menu_paste": self.on_menu_paste, \
            "on_menu_undo" : self.on_menu_undo, \
            "on_menu_redo" : self.on_menu_redo, \
            "on_R_console_entry_activate" : \
            (self.on_console_entry, self.glade_gui.get_widget("R_console_entry")), \
            "on_R_console_key_event" : self.on_console_key_event, \
            "on_open_script_open_button" : self.on_open_script_open_button,\
            "on_open_script_cancel_button" : self.on_open_script_cancel_button,\
            "on_export_script_save_button" : self.on_export_script_save_button,\
            "on_export_script_cancel_button" : self.on_export_script_cancel_button,\
            "on_Data_objects_tree_cursor_changed": self.on_data_objects_selected,\
            "on_Data_objects_remove" : self.on_data_objects_remove,\
            "on_error_button" : self.on_error_button}
        self.glade_gui.signal_autoconnect(main_callbacks)

        self.export_script_dialog = self.glade_gui.get_widget("Export_script_file_chooser")
        self.open_script_dialog = self.glade_gui.get_widget("Open_script_file_chooser")
        self.error_dialog = self.glade_gui.get_widget("Error_dialog")
        self.error_label = self.glade_gui.get_widget("Error_label")
        self.data_objects_edit_button = self.glade_gui.get_widget("Data_objects_edit")
        self.data_objects_remove_button = self.glade_gui.get_widget("Data_objects_remove")
    
def main():
    """The main function"""
    # Enter the main Gtk loop
    gtk.main()
    return 0
        
if __name__ == "__main__":
    MajeRe_Gui()
    main()  
